(ns zesql.tags
  (:refer-clojure :exclude [read-string replace])
  (:require [zesql.support.selmer :refer [add-tag!]]
            [clojure.edn :refer [read-string]]
            [clojure.string :refer [trim lower-case blank? replace join]]))

(defn- trim-word [^String word]
  {:pre [(not (blank? word))]}
  (fn [^String s]
    (if-not (blank? s)
      (let [count (.length word)
            temp  (lower-case s)
            s     (if (.startsWith temp word) (.substring s count) s)
            len   (.length s)
            temp  (lower-case s)
            s     (if (.endsWith temp word) (.substring s 0 (- len count)) s)]
        s)
      s)))

(def ^:primary trim-and (trim-word "and"))

(def ^:primary trim-or (trim-word "or"))

(def ^:primary trim-comma (trim-word ","))

(defn- clean [s]
  (replace s #"\s{2,}" " "))


;;;;;;;;;;; Order By
(defn- single? [v]
  (and (coll? v)
    (not-any? coll? v)))

(defn- multi? [v]
  (and (coll? v)
    (some coll? v)))

(defn- to-str [v]
  (cond
    (or (keyword? v) (symbol? v)) (name v)
    (fn? v) (to-str (v))
    :else (str v)))

(defn- to-single [v]
  (cond
    (map? v) (let [{:keys [field order]} v]
               [(to-str field) (to-str order)])
    (coll? v) (let [[field order] v]
                [(to-str field) (to-str order)])
    (fn? v) (v)
    :else [(to-str v)]))

(defmulti order-clause
  (fn [opt]
    (cond
      (single? opt) :single
      (multi? opt) :multi
      (fn? opt) :fn)))

(defmethod order-clause :default [opt]
  (to-str opt))

(defmethod order-clause :single [[field order & _]]
  (str (to-str field) (when order (str " " (to-str order)))))

(defmethod order-clause :multi [opt]
  (->> opt
    (map to-single)
    (map order-clause)
    (filter (complement blank?))
    (join ", ")))

(defmethod order-clause :fn [f]
  (order-clause (f)))

(comment
  (prn (order-clause "name"))
  (prn (order-clause :name))
  (prn (order-clause []))
  (prn (order-clause ["name"]))
  (prn (order-clause ["name" "asc"]))
  (prn (order-clause ["name" :desc]))
  (prn (order-clause ["id" ["name" "desc"]]))
  (prn (order-clause ["id" ["name" "desc"] :age]))
  (prn (order-clause [[:id :desc] ["name" "desc"]]))
  (prn (order-clause [[:id :desc] {:field :name, :order :asc}]))

  (prn (order-clause (fn [] [:id :asc])))
  (prn (order-clause (fn [] [[:id :asc] :name])))
  (prn (order-clause [(fn [] [:id :asc]) {:field :name, :order :asc}])))

(add-tag! :order
  (fn [[n] context-map]
    (when-let [k (keyword n)]
      (let [s (order-clause (get context-map k))]
        (when-not (blank? s)
          (str " ORDER BY " s))))))
;;;;;;;;;;;;; / Order By

(add-tag! :order-by
  (fn [args context-map]
    (let [vf    (fn [field] (if (string? field) field (context-map (keyword field))))
          field (some-> args first read-string vf name)
          dir   (some-> args second read-string vf name)]
      (when-not (blank? field)
        (str " ORDER BY " field
          (when-not (blank? dir)
            (str " " dir)))))))

(add-tag! :where
  (fn [_args _context-map m]
    (let [content (-> m (get-in [:where :content]) trim trim-and trim-or clean trim)]
      (when-not (blank? content)
        (str " WHERE " content))))
  :endwhere)

(add-tag! :set
  (fn [_args _context-map m]
    (let [content (-> m (get-in [:set :content]) trim trim-comma clean trim)]
      (when-not (blank? content)
        (str " SET " content))))
  :endset)